package window

import (
	"sync"
	"time"
)

// NewRollingPolicy 创建滑动窗口策略
func NewRollingPolicy(bucketDuration time.Duration, bucketSize int) Policy {
	return &rollingPolicy{
		mu:             sync.RWMutex{},
		window:         NewWindow(bucketSize),
		bucketDuration: bucketDuration,
		lastOpTime:     time.Now(),
		size:           bucketSize,
		prevOffset:     0,
	}
}

type rollingPolicy struct {
	mu             sync.RWMutex
	window         *Window
	bucketDuration time.Duration // 每个桶的时间区间大小
	lastOpTime     time.Time     // 最后一次的操作时间
	size           int           // 桶的个数
	prevOffset     int           // 上次使用的桶偏移量
}

func (r *rollingPolicy) Append(val float64) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.updateStat()
	r.window.Append(r.prevOffset, val)
}

func (r *rollingPolicy) Add(val float64) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.updateStat()
	r.window.Add(r.prevOffset, val)
}

func (r *rollingPolicy) Reduce(f func(Iterator) float64) float64 {
	r.mu.RLock()
	defer r.mu.RUnlock()
	span := r.span()
	count := r.size - span
	if count < 1 { // 当跨度大于等于窗口大小时，数据已经过期不能使用
		return 0
	}
	// 开始迭代位置，加上span表示当前桶，span大于0就是一个新桶就不需要迭代，等于0就和上次操作的桶是一个
	// 迭代不能直接遍历所有的桶，因为迭代时距离上次操作时间有一定的桶时间跨度，所以迭代桶的数量是有可能小于桶的总数量的
	// 从+span+1的位置开始迭代最后再回到上次操作的位置就完成了迭代
	start := r.prevOffset + span + 1
	if start >= r.size {
		start -= r.size
	}
	return f(r.window.Iterator(start, count))
}

// 重制桶
func (r *rollingPolicy) resetBuckets(span int) {
	r.window.ResetBuckets((r.prevOffset+1)%r.size, span)
}

// 获取本次操作和上次操作的跨度
func (r *rollingPolicy) span() int {
	span := int(time.Since(r.lastOpTime) / r.bucketDuration)
	if span >= 0 && span < r.size {
		return span
	}
	return r.size
}

func (r *rollingPolicy) updateStat() {
	span := r.span()
	if span > 0 {
		r.resetBuckets(span)
		r.prevOffset = (r.prevOffset + span) % r.size
		// 这里不需要精确记录操作时间，只要这个时间落在同一个桶中结果都是一样的
		r.lastOpTime = r.lastOpTime.Add(time.Duration(span) * r.bucketDuration)
	}
}
